Functions/Get-ADDirSyncChange.ps1
<# .SYNOPSIS Use the DirSync function of Active Directory to monitor the changes. .DESCRIPTION Use the DirSync function of Active Directory to monitor the changes. To use this functionality, the user needs 'Domain Administrators' privileges or the 'Replication directory changes' is delegated to the user on the domain level. .PARAMETER Partition Define the target partition to monitor the changes. .PARAMETER ComputerName Define the computer names for the Domain Controllers where the changes were monitored. .PARAMETER FilterRegex Filter the objects inside the partition based on the distinguished name with a regex filter. With the default filter value, all objects will be displayed. .PARAMETER FilterWildcard Filter the objects inside the partition based on the distinguished name with a wildcard filter. With the default filter value, all objects will be displayed. .PARAMETER CookieFile The path to the cookie file. If the cookie exist, the current USN number per domain controller will be loaded and the update scripts starts at this USN number. If no file exists, a new cookie file will be created. If no path will be specified, no cookie file will be used. .PARAMETER CookieReadOnly If this switch is specified, the cookie file will only be readed and not updated with the new USN numbers. .PARAMETER Once The script executs only one search loop. .EXAMPLE C:\> Get-ADDirSyncChange Start change monitoring with default values. .EXAMPLE C:\> Get-ADDirSyncChange -Partition 'DC=adds,DC=contoso,DC=com' -ComputerName 'DC21.adds.contoso.com' Endless change monitoring every second inside the given partition targeting the DC21 domain controller. .EXAMPLE C:\> Get-ADDirSyncChange -Partition 'DC=adds,DC=contoso,DC=com' -ComputerName 'DC21.adds.contoso.com' -CookieFile 'cookie.xml' -Once Create a cookie file for the current state of the partition. .EXAMPLE C:\> Get-ADDirSyncChange -Partition 'DC=adds,DC=contoso,DC=com' -ComputerName 'DC21.adds.contoso.com' -CookieFile 'cookie.xml' -CookieReadOnly -Once Get all changes inside the partition which has been performed since the cookie creation. Preserve the cookie value. .EXAMPLE C:\> Get-ADDirSyncChange -Partition 'DC=adds,DC=contoso,DC=com' -FilterWildcard '*OU=Test,DC=adds,DC=contoso,DC=com' Endless change monitoring but only for the test OU. With this filter, deleting objects is not reported. .NOTES Author : Claudio Spizzi License : MIT License Source : Microsoft Exchange FAQ, Author: Frank Carius, Website: http://www.msxfaq.de This function is a complete rewrite of the original script 'Get-ADChanges' provided by Frank Carius. Credits for initial concept and idea goes to him. .LINK https://github.com/claudiospizzi/ActiveDirectoryFever .LINK http://www.msxfaq.de/tools/exprivat/get-adchanges.htm #> function Get-ADDirSyncChange { [CmdletBinding()] param ( [Parameter(Mandatory=$false)] [String] $Partition = ([ADSI] "LDAP://$env:USERDOMAIN/RootDSE").DefaultNamingContext, [Parameter(Mandatory=$false)] [String[]] $ComputerName = ([ADSI] "LDAP://$env:USERDOMAIN/RootDSE").DnsHostName, [Parameter(Mandatory=$false)] [String] $FilterRegex = ".*", [Parameter(Mandatory=$false)] [String] $FilterWildcard = "*", [Parameter(Mandatory=$false)] [AllowEmptyString()] [String] $CookieFile = "", [Parameter(Mandatory=$false)] [Switch] $CookieReadOnly, [Parameter(Mandatory=$false)] [Switch] $Once ) begin { # Counter variable to display the progress bar $Count = 0 # Global hashtables to save the cookies and the serarcher objects per domain controller $Cookie = @{} $Searcher = @{} # The Active Directory properties to load $Properties = "WhenChanged", "ObjectCategory", "ObjectSID" $CommonProperties = "", "ObjectGuid", "ParentGuid", "InstanceType", "DistinguishedName", "AdsPath", "Name", "IsDeleted", "LastKnownParent", "msds-LastKnownRDN" # Check the cookie file variable # - No path specified: Do not use a cookie file and do not store the USN numbers # - Path specified, file existing: Load the latest USN numbers from the cookie file # - Path specified, file not found: Create a new cookie file but start at the lastest USN number on the Domain Controller if ($CookieFile -ne "" -and (Test-Path -Path $CookieFile)) { try { $Cookie = [Hashtable] (Import-Clixml -Path $CookieFile) } catch { Write-Error "Error while loading the cookie file: $_" return } } # Initialize the directory searcher object for each Domain Controller. Define the searcher to point # to the correct partition on the specified Domain Controllers and set the necessary properties. foreach ($Computer in $ComputerName) { # If no DirSync cookie exists, create an empty array if (-not $Cookie.ContainsKey($Computer)) { $Cookie[$Computer] = @() } # Initialize directory searcher $Searcher[$Computer] = New-Object -TypeName System.DirectoryServices.DirectorySearcher -ArgumentList ([ADSI] "LDAP://${Computer}/${Partition}") $Searcher[$Computer].DirectorySynchronization = New-Object -TypeName System.DirectoryServices.DirectorySynchronization([System.DirectoryServices.DirectorySynchronizationOptions]::IncrementalValues, [System.Convert]::FromBase64String($Cookie[$Computer])) $Searcher[$Computer].Filter = "(objectClass=*)" # If the DirSync cookie is empty, initialize the DirSync cookie to the current value if ($Cookie[$Computer].Count -eq 0) { # Set the filter temporary to exclude all objects, load all objects (ignore the result) and reset the filter $Searcher[$Computer].Filter = "(objectClass=#)" $Searcher[$Computer].FindAll() | Out-Null $Searcher[$Computer].Filter = "(objectClass=*)" # Update the cookie $Cookie[$Computer] = [System.Convert]::ToBase64String($Searcher[$Computer].DirectorySynchronization.GetDirectorySynchronizationCookie()) } } } process { do { # Iteratin Level 1: Computer foreach ($Computer in $ComputerName) { Write-Progress -Activity "Active Directory Update Searcher..." -Status "Search for DirSync changes in $Partition on $Computer" -PercentComplete (($Count++) % 100) # Execute the search for DirSync changes $SearchResult = $Searcher[$Computer].FindAll() # Filter search result with input filter (regex & wildcard) $SearchFilter = $SearchResult | Where-Object { $_.Properties["DistinguishedName"] -like $FilterWildcard -and $_.Properties["DistinguishedName"] -match $FilterRegex } # Iterating all result objects, parse the properties and return a object to the pipeline if ($SearchFilter.Count -gt 0) { # Iteratin Level 2: Search result objects foreach ($SearchObject in $SearchFilter) { # Verify if the object is deleted if ($SearchObject.Path -like "*,CN=Deleted Objects,*") { # Search the deleted object $DeletedSearcher = New-Object -TypeName System.DirectoryServices.DirectorySearcher -ArgumentList ([ADSI] "LDAP://${Computer}/${Partition}") $DeletedSearcher.Tombstone = $true $DeletedSearcher.Filter = "(&(objectClass=*)(isDeleted=TRUE)(name=$($SearchObject.Properties['Name'][0])))" $SearchObjectFull = $DeletedSearcher.FindOne() } else { # Search the full object $SearchObjectFull = ([ADSI] "LDAP://$Computer/$($SearchObject.Properties["DistinguishedName"][0])") } # Define common properties for the change object $CommonParameter = @{ Timestamp = $SearchObjectFull.Properties["WhenChanged"][0] ObjectClass = $SearchObjectFull.Properties["ObjectCategory"][0] + "" ObjectGuid = $SearchObject.Properties["ObjectGUID"][0] ObjectSid = (New-Object -TypeName "System.Security.Principal.SecurityIdentifier" -ArgumentList ($SearchObjectFull.Properties["ObjectSID"][0]) , 0) Identity = $SearchObject.Properties["DistinguishedName"][0] Account = $SearchObjectFull.Properties["SamAccountName"][0] } if ($SearchObject.Properties.Contains("IsDeleted") -and $SearchObject.Properties.Item("IsDeleted") -eq $true) { # Action: DELETE New-ADChangeObject @CommonParameter -Action DELETE -Field "LastKnownDN" -Value "CN=$($SearchObject.Properties.Item('msds-LastKnownRDN')),$($SearchObject.Properties.Item('LastKnownParent'))" } elseif ($SearchObject.Properties.Contains("IsDeleted")) { # Action: RESTORE New-ADChangeObject @CommonParameter -Action RESTORE -Field "ParentGuid" -Value ([String] [Guid] $SearchObject.Properties.Item("ParentGuid")[0]) } elseif ($SearchObject.Properties.Contains("ParentGuid") -and $SearchObject.Properties.Contains("WhenCreated")) { # Action: CREATE New-ADChangeObject @CommonParameter -Action CREATE -Field "ParentGuid" -Value ([String] [Guid] $SearchObject.Properties.Item("ParentGuid")[0]) } elseif ($SearchObject.Properties.Contains("ParentGuid")) { # Action: MOVE New-ADChangeObject @CommonParameter -Action MOVE -Field "ParentGuid" -Value ([String] [Guid] $SearchObject.Properties.Item("ParentGuid")[0]) } # Iterating all modified properties without common properties foreach ($Property in ($SearchObject.Properties.PropertyNames | Where-Object { $CommonProperties -notcontains $_ })) { switch -Wildcard ($Property) { "member;range=1-1" { foreach ($Member in $SearchObject.Properties.Item("member;range=1-1")) { # Action: MEMBER ADD New-ADChangeObject @CommonParameter -Action MEMBER-ADD -Field "Member" -Value $Member # Action: MEMBEROF ADD # TODO } break } "member;range=0-0" { foreach ($Member in $SearchObject.Properties.Item("member;range=0-0")) { # Action: MEMBER REMOVE New-ADChangeObject @CommonParameter -Action MEMBER-REMOVE -Field "Member" -Value $Member # Action: MEMBEROF REMOVE # TODO } break } "member;range=*" { # Action: MEMBER ERROR New-ADChangeObject @CommonParameter -Action UNKNOWN -Field $Property -Value $SearchObject.Properties.Item($Property) break } "*;range=1-1" { # Action: CUSTOM ADD New-ADChangeObject @CommonParameter -Action CUSTOM-ADD -Field $Property.Replace(';range=1-1', '') -Value $SearchObject.Properties.Item($Property) break } "*;range=0-0" { # Action: CUSTOM REMOVE New-ADChangeObject @CommonParameter -Action CUSTOM-REMOVE -Field $Property.Replace(';range=1-1', '') -Value $SearchObject.Properties.Item($Property) break } default { # Action: MODIFY New-ADChangeObject @CommonParameter -Action MODIFY -Field $Property -Value $SearchObject.Properties.Item($Property) } } } } # Update cookie value $Cookie[$Computer] = [System.Convert]::ToBase64String($Searcher[$Computer].DirectorySynchronization.GetDirectorySynchronizationCookie()) } # Update the cookie file if necessary if (($CookieFile -ne "") -and (-not $CookieReadOnly)) { $Cookie | Export-Clixml -Path $CookieFile -Encoding Unicode } } Start-Sleep -Seconds 1 } until ($Once) } end { } } |